package com.google.code.virtualhockey.vhx.sds

import com.google.appengine.api.datastore.Key
import com.google.appengine.api.datastore.Entity
import java.lang.StringBuilder

/**
 * Indicates that the given Scala class' instances can be persisted using the Google App Engine.
 * Provides support for a persisted entity's ID and a potentially existing parent.
 *
 * @param A the class that implements this trait
 */
protected[sds] trait STEntity[A <: STEntity[A]] {
  protected type TId
  protected type TKey <: SKey[A]

  private[sds] var _id: Option[TId] = None
  private[sds] var _parent: Option[SKey[_]] = None
  lazy val metadata: STMetaData[A] = Sds.schema.get.getMetaData( getClass().asInstanceOf[Class[A]] )

  /**
   * Public getter for the ID.
   */
  def id(): Option[TId] = _id

  /**
   * Public getter for the parent key.
   */
  def parent(): Option[SKey[_]] = _parent

  /**
   * Returns the key that currently uniquely identifies this instance.
   * Returns an instance of SKey if the entity's ID field has been set.
   */
  def getKey(): TKey

  /**
   * Saves this object into the GAE datastore using the current default settings.
   * Returns the new key of this object.
   *
   * @return the new key of this object
   */
  def put(): TKey = put( Sds() )

  /**
   * Saves this object into the GAE datastore using the given Sds instance.
   * Returns the new key of this object.
   *
   * @param sds the Sds instance to use (required to access the actual GAE data store services)
   * @return the new key of this object
   */
  def put( sds: Sds ): TKey = {
    val keyNew = sds.service.put( if ( sds.txn == None ) null else sds.txn.get, toGae )
    updateFromKey( keyNew )
    getKey
  }

  /**
   * Deletes this entity from the GAE data store. Uses the default Sds object.
   */
  def delete(): Unit = delete( Sds() )

  /**
   * Deletes this entity from the GAE data store.
   *
   * @param sds the Sds instance to use (required to access the actual GAE data store services)
   */
  def delete( sds: Sds ): Unit = {
    sds.service.delete( if ( sds.txn == None ) null else sds.txn.get, getKey().toGaeKey )
  }

  /**
   * Initialises this object by using the given GAE Entity.
   *
   * @param ent the GAE entity from which to initialise this object
   * @param metadata the meta data providing access to the information which fields need to be set
   */
  private[sds] def fromGae( ent: Entity ) {
    setIdFromKey( ent.getKey )
    setParentFromKey( ent.getKey )
    metadata.fields.foreach( ( f ) => { f.setFromGae( this, ent ) } )
  }

  /**
   * Converts this instance into a GAE compatible Entity
   */
  private[sds] def toGae(): Entity = {
    val ent = createGaeEntity
    metadata.fields.foreach( ( f ) => { f.setToGae( ent, this ) } )
    ent
  }

  /**
   * Creates a new GAE Entity instance. The created Entity only has a set key (if an ID is already
   * available) but no property is set.
   */
  protected def createGaeEntity(): Entity

  /**
   * Updates the ID or name and the parent from the given GAE key.
   *
   * @param key the Key of the corresponding GAE entity
   */
  protected def updateFromKey( key: Key ): Unit = {
    setIdFromKey( key )
    setParentFromKey( key )
  }

  /**
   * Sets this object's ID using the given GAE Key object.
   *
   * @param key the Key of the corresponding GAE entity
   */
  protected def setIdFromKey( key: Key ): Unit

  /**
   * Sets the parent key if the given key refers to a parent GAE Key.
   *
   * @param key the Key of the corresponding GAE entity from which the parent key is extracted (if any)
   */
  protected def setParentFromKey( key: Key ): Unit = {
    val parent = key.getParent
    if ( parent == null )
      this._parent = None
    else
      this._parent = Some( SKey.fromGaeKey( parent ) )
  }

  /**
   * Default implementation of toString() that displays the class' simple name together
   * with the ID value (if there is an ID set).
   */
  override def toString = {
    if ( _id == None )
      metadata.kind + "( id: None )"
    else
      metadata.kind + "( id: " + this._id.get.toString + " )"
  }

  /**
   * Generates an output that combines the entity's ID and all meaningful field values.
   */
  def toStringLong = {
    val attrs = new StringBuilder( "id: " )

    // id
    //
    attrs.append( if ( _id == None ) "None" else _id.get.toString )

    // other field values (if they should be displayed)
    //
    metadata.fields.filter( _._showInToString ).foreach( f => {
      val value = f.getValue( this )
      attrs.append( ", " ).append( f.logicalName ).append( ": " ).append( if ( value == None ) "None" else value.get.toString )
    } )
    
    metadata.kind + "( " + attrs.toString + " )"
  }
}

/**
 * This trait must be implemented by all classes that would like to be persistable to the GAE
 * and that have a String as unique identifier.
 */
trait STNamedEntity[A <: STEntity[A]] extends STEntity[A] {
  protected type TId = String
  protected type TKey = SNameKey[A]

  /**
   * Allows to set the name of the entity if it is not yet set. A name cannot be changed anymore once set
   * to a non null value.
   */
  protected def setEntityName( name: TId ) {
    if ( this._id == None )
      this._id = Option( name )
    else
      sys.error( "Attempted to set name to an object that already has a set name." )
  }

  override protected def setIdFromKey( key: Key ) {
    this._id = Option( key.getName )
  }

  override protected def createGaeEntity(): Entity = new Entity( getKey.toGaeKey )

  override def getKey: TKey = new SNameKey[A]( this._parent, getClass().asInstanceOf[Class[A]], id.get )
}

/**
 * This trait must be implemented by all classes that would like to be persistable to the GAE
 * and that have a numeric ID as unique identifier.
 */
trait STNumberedEntity[A <: STEntity[A]] extends STEntity[A] {
  protected type TId = Long
  protected type TKey = SIdKey[A]

  /**
   * Allows to set the ID of the entity if it is not yet set. An ID cannot be changed anymore once set
   * to a non null value.
   */
  protected def setEntityId( id: Option[TId] ) {
    if ( this._id == None )
      this._id = id
    else
      sys.error( "Attempted to set id to an object that already has a set id." )
  }

  override protected def setIdFromKey( key: Key ) {
    this._id = Option( key.getId )
  }

  override protected def createGaeEntity(): Entity = {
    // if we do not yet have a set ID value, we create an Entity without Key
    //
    if ( this._id == None )
      if ( this._parent == None )
        new Entity( metadata.kind )
      else
        new Entity( metadata.kind, this._parent.get.toGaeKey )
    else
      new Entity( getKey.toGaeKey )
  }

  override def getKey: TKey = {
    if ( this._id == None )
      sys.error( "Cannot return SKey as the object has not yet obtained an ID." )

    new SIdKey[A]( this._parent, getClass().asInstanceOf[Class[A]], id.get )
  }

  /**
   * Overridden to make sure that the deleted entity's ID is set None as numbered entities
   * can only have an ID if they are persisted to the GAE data store.
   */
  override def delete( sds: Sds ): Unit = {
    super.delete( sds )
    this._id = None
  }
}
